package com.party.web.utils.logback;

import ch.qos.logback.classic.db.DBHelper;
import ch.qos.logback.classic.db.names.DBNameResolver;
import ch.qos.logback.classic.db.names.DefaultDBNameResolver;
import ch.qos.logback.classic.spi.CallerData;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.db.DBAppenderBase;
import com.google.common.base.Preconditions;
import com.party.common.utils.HttpServletRequestUtils;
import com.party.common.utils.LogUtils;
import com.party.core.model.log.BehaviorType;
import com.party.core.model.log.LogType;
import com.party.core.model.member.Member;
import nl.bitwalker.useragentutils.UserAgent;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;

import static ch.qos.logback.core.db.DBHelper.closeStatement;
import static org.apache.shiro.util.JdbcUtils.closeConnection;

/**
 * 自定义数据输入appender
 * Created by wei.li
 *
 * @date 2017/8/23 0023
 * @time 10:14
 */
public class DBAppender extends DBAppenderBase<ILoggingEvent> {

    protected String insertSQL;
    protected static final Method GET_GENERATED_KEYS_METHOD;

    private DBNameResolver dbNameResolver;

    private boolean isBehavior;

    static final int TIMESTMP_INDEX = 1;
    static final int  FORMATTED_MESSAGE_INDEX  = 2;
    static final int  LOGGER_NAME_INDEX = 3;
    static final int  LEVEL_STRING_INDEX = 4;
    static final int  THREAD_NAME_INDEX = 5;
    static final int  REFERENCE_FLAG_INDEX = 6;
    static final int  ARG0_INDEX = 7;
    static final int  ARG1_INDEX = 8;
    static final int  ARG2_INDEX = 9;
    static final int  ARG3_INDEX = 10;
    static final int  CALLER_FILENAME_INDEX = 11;
    static final int  CALLER_CLASS_INDEX = 12;
    static final int  CALLER_METHOD_INDEX = 13;
    static final int  CALLER_LINE_INDEX = 14;
    static final int  EVENT_ID_INDEX  = 15;

    static final StackTraceElement EMPTY_CALLER_DATA = CallerData.naInstance();

    static {
        Method getGeneratedKeysMethod;
        try {
            getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null);
        } catch (Exception ex) {
            getGeneratedKeysMethod = null;
        }
        GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
    }

    public void setDbNameResolver(DBNameResolver dbNameResolver) {
        this.dbNameResolver = dbNameResolver;
    }

    @Override
    public void start() {
        if (dbNameResolver == null)
            dbNameResolver = new DefaultDBNameResolver();
        insertSQL = ATSSQLBuilder.buildInsertSQL(dbNameResolver);
        super.start();
    }

    public void append(ILoggingEvent eventObject) {
        Connection connection = null;
        PreparedStatement insertStatement = null;
        try {
            connection = connectionSource.getConnection();
            connection.setAutoCommit(false);

            insertStatement = connection.prepareStatement(getInsertSQL());

            synchronized (this) {
                subAppend(eventObject, connection, insertStatement);
            }
            connection.commit();
        } catch (Throwable sqle) {
            addError("problem appending event", sqle);
        } finally {
            closeStatement(insertStatement);
            closeConnection(connection);
        }
    }

    @Override
    protected void subAppend(ILoggingEvent event, Connection connection,PreparedStatement insertStatement) throws Throwable {
        isBehavior = LogType.BEHAVIOR_LOG.getValue().equals(event.getLoggerName());
        bindLoggingEventWithInsertStatement(insertStatement, event);
        bindLoggingEventArgumentsWithPreparedStatement(insertStatement, event.getArgumentArray());
        bindCallerDataWithPreparedStatement(insertStatement, event.getCallerData());
        int updateCount = insertStatement.executeUpdate();
        if (updateCount != 1) {
            addWarn("Failed to insert loggingEvent");
        }
    }

    protected void secondarySubAppend(ILoggingEvent event,
                                      Connection connection,long eventId) throws Throwable {
    }

    void bindLoggingEventWithInsertStatement(PreparedStatement stmt,
                                             ILoggingEvent event) throws SQLException {
        stmt.setTimestamp(TIMESTMP_INDEX, new Timestamp(event.getTimeStamp()));
        stmt.setString(FORMATTED_MESSAGE_INDEX, event.getFormattedMessage());
        stmt.setString(LOGGER_NAME_INDEX, event.getLoggerName());
        stmt.setString(LEVEL_STRING_INDEX, event.getLevel().toString());
        stmt.setString(THREAD_NAME_INDEX, event.getThreadName());
        stmt.setShort(REFERENCE_FLAG_INDEX, DBHelper.computeReferenceMask(event));
        if (isBehavior){
            stmt.setInt(16, 1);
            Member member = (Member) event.getArgumentArray()[0];
            BehaviorType behaviorType = (BehaviorType) event.getArgumentArray()[1];
            String message = event.getMessage()
                    .replaceFirst ("\\{\\}",member.getRealname()).replaceFirst("\\{\\}", behaviorType.getValue());
            if (event.getArgumentArray().length > 2 && null != event.getArgumentArray()[2]){
             message = message.replaceFirst("\\{\\}", event.getArgumentArray()[2].toString());
            }
            stmt.setString(FORMATTED_MESSAGE_INDEX, message);
        }
        else {
            ThrowableProxy proxy = (ThrowableProxy)event.getThrowableProxy();
            if (null != proxy && null != proxy.getThrowable()){
                Throwable throwable = proxy.getThrowable();
                StringBuilder stringBuilder = new StringBuilder(event.getMessage()).append(LogUtils.getTrace(throwable));
                stmt.setString(FORMATTED_MESSAGE_INDEX, stringBuilder.toString());
            }
            stmt.setInt(16, 0);
        }
    }

    void bindLoggingEventArgumentsWithPreparedStatement(PreparedStatement stmt,
                                                        Object[] argArray) throws SQLException {
        if (isBehavior){
            Preconditions.checkElementIndex(1, argArray.length, "参数数组长度非法");
            HttpServletRequest request
                    = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));

            Member member = (Member) argArray[0];
            BehaviorType behaviorType = (BehaviorType) argArray[1];
            stmt.setString(17, member.getId());
            stmt.setString(18, behaviorType.getCode());
            if (argArray.length > 2 && argArray[2] != null){
                stmt.setString(19, asStringTruncatedTo254(argArray[2]));
            }
            else {
                stmt.setString(19, null);
            }
            stmt.setString(20, userAgent.getBrowser().toString());
            stmt.setString(21, HttpServletRequestUtils.getRemoteIpAddress(request));
            stmt.setString(22, userAgent.getOperatingSystem().toString());
            stmt.setString(23, request.getRequestURL().toString());
            stmt.setString(24, request.getMethod());
        }
        else {
            stmt.setString(17, null);
            stmt.setString(18, null);
            stmt.setString(19, null);
            stmt.setString(20, null);
            stmt.setString(21, null);
            stmt.setString(22, null);
            stmt.setString(23, null);
            stmt.setString(24, null);
        }
        int arrayLen = argArray != null ? argArray.length : 0;

        for(int i = 0; i < arrayLen && i < 4; i++) {
            stmt.setString(ARG0_INDEX+i, asStringTruncatedTo254(argArray[i]));
        }
        if(arrayLen < 4) {
            for(int i = arrayLen; i < 4; i++) {
                stmt.setString(ARG0_INDEX+i, null);
            }
        }
    }

    String asStringTruncatedTo254(Object o) {
        String s = null;
        if(o != null) {
            s= o.toString();
        }

        if(s == null) {
            return null;
        }
        if(s.length() <= 254) {
            return s;
        } else {
            return s.substring(0, 254);
        }
    }

    void bindCallerDataWithPreparedStatement(PreparedStatement stmt,
                                             StackTraceElement[] callerDataArray) throws SQLException {
        StackTraceElement caller = extractFirstCaller(callerDataArray);
        stmt.setString(CALLER_FILENAME_INDEX, caller.getFileName());
        stmt.setString(CALLER_CLASS_INDEX, caller.getClassName());
        stmt.setString(CALLER_METHOD_INDEX, caller.getMethodName());
        stmt.setString(CALLER_LINE_INDEX, Integer.toString(caller.getLineNumber()));
        stmt.setInt(15, 0);
    }

    private StackTraceElement extractFirstCaller(StackTraceElement[] callerDataArray) {
        StackTraceElement caller = EMPTY_CALLER_DATA;
        if(hasAtLeastOneNonNullElement(callerDataArray))
            caller = callerDataArray[0];
        return caller;
    }

    private boolean hasAtLeastOneNonNullElement(StackTraceElement[] callerDataArray) {
        return callerDataArray != null && callerDataArray.length > 0 && callerDataArray[0] != null;
    }

    @Override
    protected Method getGeneratedKeysMethod() {
        return GET_GENERATED_KEYS_METHOD;
    }

    @Override
    protected String getInsertSQL() {
        return insertSQL;
    }

}
